Zum Hauptinhalt springen
Version: Aktuellste Version

SalaryElements (Lohnelemente) – Entwicklerdokumentation

Diese Dokumentation beschreibt das Feature SalaryElements (deutsch „Lohnelemente") der biz.Suite auf dem Branch salary_module_extension. Ein SalaryElement ist ein konkreter, datierter Lohnposten eines Mitarbeiters für eine bestimmte Lohnart (SalaryType).

Wichtig – zwei getrennte Generierungs-Subsysteme: Auf diesem Branch existieren zwei voneinander unabhängige Mechanismen, die SalaryElements erzeugen. Sie dürfen nicht verwechselt werden:

  1. Journalbasierte Generierung (das eigentliche neue Feature dieses Branches) – der SalaryElementGenerationProcessor erzeugt Lohnelemente tagesgenau aus abgerechneten und freigegebenen Arbeitszeitsjournalen, Buchungen, Abwesenheiten und den Regeln eines Lohnvertrags (SalaryAgreement).
  2. Zulagen-Monatsgenerierung (Altbestand, weiterhin vorhanden) – der GenerateMounthlySalaryElementsCommand erzeugt monatliche Zulagen-Posten aus WorkerSalaryType-Zuordnungen vom Typ allowance.

In beiden Fällen entsteht dieselbe Entity SalaryElement, das Feld salaryUnit wird aber semantisch unterschiedlich befüllt (Subsystem 1: Dauer in Sekunden; Subsystem 2: der ganzzahlige defaultValue der Zuordnung).

Sie richtet sich an Backend- und Frontend-Entwickler, die das Feature pflegen, erweitern oder integrieren.

Stand: 2026-07-10 Branch: salary_module_extension (Backend & Frontend) Repositorys:

  • Backend: biz.Suite_Backend (Symfony / Doctrine / PHP)
  • Frontend: biz.Suite_Frontend (Angular / TypeScript)

Verwandte Dokumentation: Nachtschicht-Entwicklerdokumentation (das 12_12-Modell, auf dem die Nachtschicht-Behandlung des Generators aufbaut), Lohnmodul-Benutzerhandbuch.


Inhaltsverzeichnis​

  1. Übersicht & Motivation
  2. Datenmodell & Entities
  3. Datenbank-Migrationen
  4. Journalbasierte Generierung (SalaryElementGenerationProcessor)
  5. Auslösung / Triggering der journalbasierten Generierung
  6. Zulagen-Monatsgenerierung (Altpfad)
  7. Backend-Komponenten (Controller & Repositories)
  8. Frontend-Komponenten im Detail
  9. Berechtigungssteuerung
  10. Durchgerechnetes Beispiel
  11. Fallstricke, Grenzen & offene Punkte
  12. Referenz: alle Fundstellen

1. Übersicht & Motivation​

Das Lohnmodul soll aus den erfassten Arbeitszeiten eines Mitarbeiters die abrechnungsrelevanten Lohnposten ableiten – z. B. „8 h Normalzeit", „2 h Nachtzuschlag", „1 Feiertag" oder „Urlaub". Diese Posten sind die SalaryElements.

Zentrale Idee der journalbasierten Generierung:

  • Grundlage ist das abgerechnete Arbeitszeitsjournal (TimeProfileWorkerJournal) eines Tages. Nur wenn dieses berechnet (isCalculated) und freigegeben (isApproved) ist, gilt der Tag als „fertig" und wird generiert.
  • Die realen Stempelungen (TimeControlBooking) werden in Anwesenheits-Intervalle zerlegt.
  • Ein Lohnvertrag (SalaryAgreement) definiert pro Wochentag Zeitfenster (SalaryAgreementLine), die jeweils eine Lohnart (SalaryType) tragen (z. B. „22:00–06:00 → Nachtzuschlag").
  • Die Überlappung von Anwesenheit und Vertrags-Zeitfenster ergibt je Lohnart eine Dauer in Sekunden, die – abzüglich Pausen – als SalaryElement gespeichert wird.
  • Abwesenheiten (Absence) an dem Tag erzeugen eigene Elemente über die Lohnart des jeweiligen Abwesenheitscodes (AbsenceCode.salaryType).
  • Feiertage und Nachtschicht (12_12) werden gesondert behandelt.

Der Altpfad (Zulagen) bleibt davon unberührt: Er erzeugt weiterhin monatliche Pauschal-Zulagen aus WorkerSalaryType-Zuordnungen (Kap. 6).


2. Datenmodell & Entities​

2.1 Überblick der Beziehungen​

                 ┌───────────────────────────┐
│ SalaryAgreement │ Lohnvertrag (name, description)
│ wageSalary/overTimeSalary│ + 3 SalaryType-Referenzen
│ /defaultSalary │
└───┬───────────────┬───────┘
OneToMany │ │ OneToMany workers (Worker.salaryAgreement)
salaryAgreementDays│ ▼
▼ ┌────────────┐
┌────────────────────┐│ Worker │──OneToMany──► SalaryElement
│ SalaryAgreementDay ││ salaryAgree│ (worker)
│ name = Wochentag ││ ment │
└─────────┬──────────┘└─────┬──────┘
OneToMany │ │ (Journal, Bookings, Absences …)
salaryAgreementLines │
▼ ▼
┌───────────────────────────────────────┐ ┌──────────────────────┐
│ SalaryAgreementLine │ │ TimeProfileWorker │
│ startTime/endTime, timeProfileType, │ │ Journal │
│ salaryType, minimalTime/maximalTime, │ │ isCalculated, │
│ secondarySalaryType, secondaryFactor │ │ isApproved, │
└──────────────────┬─────────────────────┘ │ profileTime, date │
ManyToOne │ salaryType └──────────────────────┘
▼
┌────────────────────────┐ ┌────────────────────┐
│ SalaryType │◄────────│ AbsenceCode │
│ type, amount, │ salary │ salaryType (NEW) │
│ minimalTime, │ Type └────────────────────┘
│ isStandardForHoliday, │
│ absenceCodes (OneToMany)│
└─────────┬──────────────┘
OneToMany│ salaryElements
▼
┌────────────────────────────────────────────┐
│ SalaryElement │
│ profileDate (Kalendertag des Postens) │
│ journalDate (Tag des erzeugenden Journals) │ ← NEU
│ salaryUnit (Dauer in Sekunden bzw. Wert) │
│ transferToPayroll, salaryType, worker │
└─────────────────────────────────────────────┘

(Altpfad Zulagen: WorkerSalaryType = Worker ↔ SalaryType + defaultValue)

2.2 Entity SalaryElement​

Datei: biz.Suite_Backend/src/Entity/SalaryElement.php

FeldTypBedeutung
idintPK
profileDatedatetime, nullableKalendertag, dem der Posten zugerechnet wird. Bei Nachtschicht kann ein Lauf Posten auf zwei Kalendertage schreiben.
journalDatedatetime, nullableNEU. Datum des Journals, das den Posten erzeugt hat. Dient der idempotenten Neu-Generierung (siehe Kap. 4.2).
salaryUnitfloat, nullableJournalpfad: Dauer in Sekunden. Zulagenpfad: der defaultValue.
transferToPayrollbool, default falseFreigabe zur Lohnabrechnung (manuell in UI).
salaryTypeManyToOne SalaryTypeLohnart des Postens.
workerManyToOne Worker (workerid)Mitarbeiter.

journalDate vs. profileDate: profileDate ist der Tag, für den der Posten fachlich zählt; journalDate markiert, welcher Generierungslauf ihn besitzt. Beide sind identisch, außer bei Nachtschichten, wo ein Journal-Tag (journalDate = T) Posten auf T-1 und T (profileDate) verteilen kann. Das Feld existiert nur im Backend – das Frontend kennt und zeigt nur profileDate.

2.3 Entity SalaryType​

Datei: biz.Suite_Backend/src/Entity/SalaryType.php

FeldTypBedeutung
descriptionstring, nullableBezeichnung
typestring, nullablehours / salary / allowance (frei; nur allowance relevant für den Altpfad)
shouldIncludeInExportboolin Export einbeziehen
externalSalaryTypestring, nullableexterne Lohnart-Kennung
amountint, nullableNEU. Zuschlag in Prozent (FE-Label „Zuschlag in %"). Wird zur Sortierung der Lohnarten genutzt (ORDER BY amount DESC), bestimmt also die Verarbeitungs-Priorität.
minimalTimefloat, nullableNEU. Mindest-Dauer (Sekunden). Erreicht die je Lohnart summierte Überlappung diesen Wert nicht, entsteht kein Posten.
isStandardForHolidaybool, default falseNEU. Kennzeichnet die Lohnart, die an Feiertagen als virtueller Ganztagsposten verwendet wird.
absenceCodesOneToMany AbsenceCodeNEU. Rückbeziehung zu den Abwesenheitscodes, die diese Lohnart nutzen.
salaryElementsOneToMany SalaryElementzugehörige Posten

2.4 Lohnvertrag: SalaryAgreement, SalaryAgreementDay, SalaryAgreementLine​

  • SalaryAgreement (SalaryAgreement.php) – name, description; OneToMany salaryAgreementDays, salaryAgreementLines, timeProfiles, workers; drei SalaryType-Referenzen wageSalary, overTimeSalary, defaultSalary. Ein Mitarbeiter wird über Worker.salaryAgreement (ManyToOne) einem Vertrag zugeordnet.

  • SalaryAgreementDay (SalaryAgreementDay.php) – name = Wochentagsname (englisch, z. B. Monday), OneToMany salaryAgreementLines. Repräsentiert die Regeln eines Wochentags.

  • SalaryAgreementLine (SalaryAgreementLine.php) – ein einzelnes Zeitfenster einer Regel:

    FeldBedeutung
    startTime / endTimeZeitfenster (nur Uhrzeit)
    salaryTypeLohnart, die für Überlappungen in diesem Fenster gutgeschrieben wird
    timeProfileTypeTyp-Zuordnung
    minimalTime / maximalTimeGrenzen (float; derzeit vom Processor nicht ausgewertet, siehe Kap. 11)
    secondarySalaryType / secondaryFactorSekundär-Lohnart / Faktor (derzeit vom Processor nicht ausgewertet)
    salaryAgreementDay / salaryAgreementZugehörigkeit

Kernrolle im Generator: Für jeden Kalendertag sucht der Processor den SalaryAgreementDay des Vertrags anhand des Wochentags und lädt dessen SalaryAgreementLines. Die Überlappung von Anwesenheit und Line-Zeitfenster wird der salaryType der Line gutgeschrieben.

2.5 AbsenceCode.salaryType (neue Verknüpfung)​

Datei: AbsenceCode.php – neu: #[ORM\ManyToOne(inversedBy: 'absenceCodes')] private ?SalaryType $salaryType. Eine Abwesenheit (Absence) verweist über ihren AbsenceCode auf eine Lohnart, mit der der Abwesenheitspfad des Generators arbeitet (Kap. 4.3).

2.6 PreCalculatedBooking (View-Model)​

Datei: src/ViewModel/PreCalculatedBooking.php

Kein Doctrine-Entity, sondern ein reines Rechenobjekt: from, to (Carbon), isBreakTime (bool), timeLeft (float, Sekunden), salaryOverlaps (array<int typeId, float sekunden>). Repräsentiert ein Anwesenheits-Intervall zwischen zwei Stempelungen sowie dessen Überlappungen pro Lohnart.

2.7 WorkerSalaryType (nur Altpfad)​

Datei: WorkerSalaryType.php – Join-Entity Worker ↔ SalaryType mit defaultValue (int). Nur für die Zulagen-Monatsgenerierung relevant (Kap. 6); der SalaryElementGenerationProcessor verwendet sie nicht.


3. Datenbank-Migrationen​

Neue/relevante Migrationen dieses Branches:

MigrationInhalt
Version20260302140928.phpsalary_type ADD amount INT, ADD minimal_time DOUBLE; workers ADD salary_agreement_id (+ FK)
Version20260319151007.phpsalary_type ADD is_standard_for_holiday TINYINT(1) DEFAULT 0
Version20260323104345.phpabsencecodes ADD salary_type_id INT (+ FK)
Version20260630141454.phpsalary_element ADD journal_date DATETIME

Bestehende Tabellen (Altbestand): salary_element, salary_type, worker_salary_type, salary_agreement, salary_agreement_day, salary_agreement_line.


4. Journalbasierte Generierung (SalaryElementGenerationProcessor)​

Datei: biz.Suite_Backend/src/Processor/Salary/SalaryElementGenerationProcessor.php

Dies ist das Herzstück des Features. Der Processor ist ein Symfony-Service (autowired mit EntityManagerInterface).

4.1 Öffentliche Einstiegspunkte​

MethodeZweck
generateSalaryElementsForDayForWorker(Carbon $day, Worker $worker)Ein Mitarbeiter, ein Tag. Flusht danach.
generateSalaryElementsForDay(Carbon $day)Ein Tag, alle Mitarbeiter vom Typ employee (WorkerRepository::getWorkersByType('employee')). Flush pro Mitarbeiter.
generateSalaryElementsForMonthForWorker(int $month, Worker $worker, ?int $year)Ein Mitarbeiter, ganzer Monat. Delegiert an …ForWorkers.
generateSalaryElementsForMonth(int $month, ?int $year)Ganzer Monat, alle employee.
generateSalaryElementsForMonthForWorkers(array $workers, int $month, ?int $year)Sammel-Einstieg: iteriert je Mitarbeiter über alle Tage des Monats (getMonthRange), flusht pro Mitarbeiter, fängt Fehler pro Mitarbeiter ab und liefert eine Liste von failures (workerId, personnelNumber, error) zurück. Arbeitet mit Worker-IDs und re-fetcht die Instanz, um auch nach einem EM-Reset weiterarbeiten zu können.

Alle Wege münden in die private Kernmethode generate(Carbon $targetDate, Worker $worker).

4.2 Die Kernmethode generate() – Voraussetzung Journal​

$journal = $journalRepo->findOneBy([
'date' => $targetDate,
'worker' => $worker,
'isCalculated' => true,
'isApproved' => true
]);

// Kein berechnetes UND freigegebenes Journal -> Tag wird übersprungen
if (!$journal) {
return;
}

✅ Direkte Antwort auf die häufige Frage: Lohnelemente werden über diesen Pfad ausschließlich für Tage erzeugt, für die ein Journal existiert, das sowohl isCalculated = true als auch isApproved = true ist. Fehlt das Journal oder ist eines der Flags nicht gesetzt, gilt der Tag als „noch nicht bereit" und es wird nichts generiert.

4.2 Idempotenz – Löschen vor Neuanlage​

Direkt nach dem Journal-Check:

$this->deleteSalaryElementsForJournalDay($worker, $targetDate);

deleteSalaryElementsForJournalDay entfernt alle Elemente, deren journalDate auf targetDate fällt (SalaryElementRepository::getByWorkerAndJournalDate). Damit ist ein erneuter Lauf für denselben Tag idempotent – auch wenn eine Nachtschicht Posten auf zwei Kalendertage (profileDate) verteilt hat, werden sie über den gemeinsamen journalDate sauber zurückgesetzt und neu erzeugt.

4.3 Abwesenheitspfad (generateFromAbsence)​

$absences = $absenceRepo->getAbsencesForWorkerInRangeByType($worker, $start, $end)->getResult();
if (count($absences) > 0) {
$this->generateFromAbsence($targetDate, $worker, $absences, $journal);
}

Für jede Abwesenheit am Tag:

  • Lohnart = absence->getAbsenceCode()?->getSalaryType() (die neue Verknüpfung aus Kap. 2.5).
  • Dauer = min(journal.profileTime, absenceDuration) – die Abwesenheit wird auf die Sollzeit des Tages gedeckelt.
  • Es entsteht ein SalaryElement mit profileDate = journalDate = targetDate, salaryUnit = duration (Sekunden).

Wichtig: Der Abwesenheitspfad läuft vor der Buchungsprüfung und unabhängig von einem Lohnvertrag. Abwesenheits-Posten entstehen also auch, wenn der Mitarbeiter keinen SalaryAgreement hat oder keine Buchungen vorliegen.

4.4 Buchungspfad – Ablauf​

Nach dem Abwesenheitspfad:

$isNightShift = $journal->getTimeProfile()?->getCalculationPeriod() === '12_12';
[$windowStart, $windowEnd] = $this->getCalculationPeriodRange($targetDate, $isNightShift);
$bookings = $bookingRepo->findInRangeFiltered($windowStart, $windowEnd, $worker);

if (count($bookings) < 1 || !$worker->getSalaryAgreement()) {
return; // ohne Buchungen ODER ohne Lohnvertrag: kein Buchungspfad
}
  1. Zeitfenster (getCalculationPeriodRange): Standard T 00:00 … T 23:59; Nachtschicht (12_12) T-1 12:00 … T 12:00.
  2. Anwesenheits-Intervalle bilden (createPreCalculatedBookings): Aufeinanderfolgende Stempelungen werden paarweise zu PreCalculatedBooking-Intervallen verbunden.
    • Beginnt eine Lücke mit einem Check-Out, ist es Abwesenheit (der Mitarbeiter ist gegangen) → wird übersprungen.
    • Ist die vorherige Stempelung weder Check-In noch Check-Out (also ein Pausen-Typ), wird das Intervall als Pause markiert (isBreakTime = true).
    • timeLeft = Länge des Intervalls in Sekunden.
  3. An Kalendertagen splitten (splitPreCalculatedBookingsAtCalendarDay): Intervalle, die über Mitternacht laufen, werden an den Tagesgrenzen zerschnitten – denn jeder Kalendertag kann eigene SalaryAgreementLine-Regeln haben.
  4. Nach Kalendertag gruppieren (slicesByDay, Schlüssel = from->toDateString()).
  5. Pro Kalendertag (foreach $slicesByDay): a. SalaryAgreementDay per Wochentag (calendarDay->dayName) und SalaryAgreementLines laden. b. Feiertag (publicHolidayRepo->findOneBy(['date' => $calendarDay])): existiert einer und gibt es eine Lohnart mit isStandardForHoliday, wird eine virtuelle Ganztags-Line (00:00–23:59) mit dieser Lohnart hinzugefügt. c. processOverlaps($daySlices, $salaryAgreementLines) – Überlappungen Anwesenheit × Line. d. sumOverlapTimes(...) – Summierung je Lohnart inkl. minimalTime-Schwelle. e. calculateBreakTime(...) – abzuziehende Pausen je Lohnart. f. createSalaryElements(...) – Posten schreiben (profileDate = calendarDay, journalDate = targetDate).

4.5 Überlappung & Summierung​

processOverlaps (Buchungen × Vertrags-Lines): Für jedes Anwesenheits-Intervall und jede Line werden Start/Ende der Line auf das Datum des Intervalls gestempelt (Ende vor Start → +1 Tag). Bei Überlappung wird die Überlappungsdauer (calculateOverlap = spätester Start bis frühestes Ende) je salaryType.id in slice->salaryOverlaps akkumuliert.

sumOverlapTimes (über nach amount DESC sortierte Lohnarten): Je Lohnart wird total = Σ min(slice.timeLeft, slice.salaryOverlaps[typeId]) gebildet. Erreicht total die salaryType.getMinimalTime()-Schwelle nicht, wird die Lohnart verworfen. Andernfalls fließt total in salaryTypeArray[typeId] ein.

4.6 Pausenberechnung (calculateBreakTime)​

Ermittelt je Lohnart die abzuziehende Pausenzeit als max(geplante Pause, tatsächlich genommene Pause):

  • Geplante Pausen stammen aus den TimeProfileDayLines des Journals (getJournalDayLines), gefiltert auf Pausen-Typen (timeProfileType->isCalcJustifiedAbsence()), überlappt mit den Vertrags-Lines über processOverlapsForDayLines. Eine geplante Pause zählt nur für die Zeit, in der der Mitarbeiter anwesend war (presentDuration).
  • Genommene Pausen stammen aus den PreCalculatedBookings mit isBreakTime = true.
  • Nachtschicht: Zeiten werden über mapToNightShiftTimeline (Uhrzeit ≥ 12:00 → Vortag) auf die Noon-to-Noon-Zeitachse projiziert; eine geplante Pause wird nur dem Kalendertag zugerechnet, auf den sie tatsächlich fällt.

4.7 Posten schreiben (createSalaryElements)​

foreach ($salaryTypeArray as $typeId => $timeLeft) {
if ($timeLeft < 1 || !$worker->getSysClient()) continue;
$salaryType = /* per id */;
$time = $timeLeft - ($breakTimes[$typeId] ?? 0); // Pausen abziehen

$salaryElement = (new SalaryElement())
->setProfileDate($profileDate) // Kalendertag der Slice
->setJournalDate($journalDate) // targetDate des Laufs
->setSalaryType($salaryType)
->setSalaryUnit($time) // Sekunden (Netto-Arbeitszeit)
->setWorker($worker);
$salaryElement->setSysClient($worker->getSysClient());
$this->entityManager->persist($salaryElement);
}

salaryUnit ist hier die Netto-Dauer in Sekunden (Überlappung minus Pause). Ein Flush erfolgt durch den jeweiligen öffentlichen Einstiegspunkt.

4.8 Nachtschicht & Feiertag – Zusammenfassung​

  • Nachtschicht (12_12): breiteres Buchungsfenster (T-1 12:00 … T 12:00) und Noon-Pivot-Projektion (mapToNightShiftTimeline), damit eine über Mitternacht laufende Schicht korrekt einem oder zwei Kalendertagen zugeordnet wird. Baut auf demselben Prinzip auf wie die Nachtschicht-Dokumentation.
  • Feiertag: virtueller Ganztagsposten über die isStandardForHoliday-Lohnart.

5. Auslösung / Triggering der journalbasierten Generierung​

5.1 REST-Endpoint (primärer Trigger)​

Datei: SalaryElementController.php (neu auf diesem Branch)

#[Route(path: 'api/salaryElements/generate/worker/{id}', name: 'generate_salary_elements_for_worker', methods: ['POST'])]
public function generateSalaryElementsForWorker(Worker $worker, Request $request, SalaryElementGenerationProcessor $processor): JsonResponse
{
$month = (int) $request->get('month');
$processor->generateSalaryElementsForMonthForWorker($month, $worker);
return new JsonResponse(null, Response::HTTP_CREATED);
}
  • POST api/salaryElements/generate/worker/{id}?month={1-12}, leerer Body, Antwort 201 Created.
  • Generiert für einen Mitarbeiter den ganzen Monat (Jahr = aktuelles Jahr, da nicht übergeben).
  • Kein Cron/Scheduler: Wie schon der Altpfad ist auch dieser Endpoint nicht zeitgesteuert – die Generierung wird benutzerinitiiert über die Oberfläche ausgelöst (siehe Kap. 8.2).

5.2 TestCommand​

Datei: src/Command/TestCommand.php injiziert den Processor und ruft ihn auf.

⚠️ Achtung – Bruchstelle: TestCommand ruft generateSalaryElementsForWorkerForDay($worker, new Carbon()) auf, diese Methode existiert im Processor jedoch nicht (dort heißt sie generateSalaryElementsForDayForWorker(Carbon $day, Worker $worker) – anderer Name und vertauschte Argumentreihenfolge). Der Command ist damit in der aktuellen Form nicht lauffähig und dient offenbar nur zu Testzwecken. Bei einer echten Nutzung/Portierung zwingend anpassen.


6. Zulagen-Monatsgenerierung (Altpfad)​

Datei: GenerateMounthlySalaryElementsCommand.php

Dieses ältere, unabhängige Subsystem existiert unverändert weiter. Command-Name: workers:generate:saleryelements:allowance (Tippfehler „Mounthly"/„salery" im Original beibehalten).

Kurzablauf:

  • Zielmonate: aktueller + 2 folgende (jeweils Monatserster als profileDate).
  • Über alle aktiven Mitarbeiter; je Mitarbeiter alle WorkerSalaryType-Zuordnungen.
  • Nur wenn salaryType.type === 'allowance': je Zielmonat ein SalaryElement, sofern noch keines existiert (findOneBy(worker, salaryType, profileDate) → idempotent).
  • salaryUnit = workerSalaryType.getDefaultValue() (int!), transferToPayroll = false, sysClient vom Worker.

Abgrenzung: Dieser Pfad kennt weder Journale noch isApproved/isCalculated, noch journalDate, noch Lohnverträge. Er setzt journalDate nicht (bleibt null) und befüllt salaryUnit mit einer Pauschale, nicht mit Sekunden. Details und Fallstricke dieses Altpfads siehe Kap. 11.


7. Backend-Komponenten (Controller & Repositories)​

7.1 SalaryElementController​

Datei: SalaryElementController.php

RouteHTTPZweck
api/salaryElementsGETPaginierte Liste; start/end oder date (auf Monat expandiert).
api/salaryElements/{id}GETEinzelnes Element.
api/salaryElements/worker/{id}GETListe eines Mitarbeiters im Bereich.
api/salaryElementsPOSTAnlegen (createOrUpdate).
api/salaryElements/{id}PUTAktualisieren (gleiche Logik wie POST).
api/salaryElements/{id}DELETELöschen (204).
api/salaryElements/generate/worker/{id}POSTNEU – journalbasierte Generierung, ?month= (Kap. 5.1).

7.2 Repositories​

  • SalaryElementRepository (Datei):
    • getByWorkerAndRange(worker|null, start, end) – Filter über profileDate (Listen/UI).
    • getByWorkerAndJournalDate(worker, journalDate) – NEU – alle Elemente eines Journal-Laufs (Filter über journalDate), Basis der Idempotenz.
  • AbsenceRepository::getAbsencesForWorkerInRangeByType – type ist jetzt optional (?string = null); ohne type entfällt der isHoliday-Filter (der Generator ruft es ohne Typ auf).
  • WorkerRepository::getWorkersByType('employee') – aktive Mitarbeiter nach Rolle (isEmployee/isContractor/…); Grundmenge der „alle Mitarbeiter"-Einstiege.
  • SalaryTypeRepository – u. a. findByWorker, sowie findBy([], ['amount' => 'DESC']) (im Processor genutzt).
  • Weitere: SalaryAgreementDayRepository, SalaryAgreementLineRepository, TimeControlBookingRepository::findInRangeFiltered, PublicHolidayRepository, TimeProfileDayRepository/TimeProfileDayLineRepository.

7.3 SalaryAgreementLineController, SalaryTypeController, WorkerSalaryTypesController​

  • SalaryAgreementLineController – CRUD der Vertrags-Lines (auf diesem Branch angepasst).
  • SalaryTypeController – CRUD + Elasticsearch-Suche api/find/salaryTypes (Filter type, excludeByWorker).
  • WorkerSalaryTypesController – CRUD der Zulagen-Zuordnungen (Altpfad).

8. Frontend-Komponenten im Detail​

Pfade relativ zu biz.Suite_Frontend/src/app/main/apps/. Zwei App-Bereiche: organization-management/salary-managment/ (Admin) und hrm/ (mitarbeiterbezogen).

8.1 Frontend-Modelle​

ModellDateiNeue/relevante Felder
SalaryTypesalary-managment/salary-types/salary-type.model.ts+ amount: number, minimal_time: number (Sekunden), is_standard_for_holiday?: boolean. Kein absence_codes.
SalaryElementsalary-managment/salary-elements/salary-element.tsunverändert; kein journal_date. salary_unit = Sekunden.
SalaryAgreementsalary-agreements/salary-agreement.model.tsname, description, salary_agreement_days?, default_salary?, wage_salary?, over_time_salary?
SalaryAgreementDaysalary-agreement-day/salary-agreement-day.tsid, name
SalaryAgreementLinesalary-agreement-day/salary-agreement-line.tsstart_time, end_time, salary_type, time_profile_type, minimal_time, maximal_time, secondary_salary_type, secondary_factor, salary_agreement(_day)
Workerhrm/workers/worker.model.ts+ salary_agreement?: SalaryAgreement
AbsenceCodehrm/absenceCode/absenceCode.model.ts+ salary_type: SalaryType

Zeitwert-Handling: salary_unit und minimal_time werden als Sekunden persistiert, aber als HH:mm (type="time") editiert. Hilfsfunktionen durationToSeconds / secondsToDuration konvertieren (in salary-element-dialog und salary-type-dialog). Die Liste rendert salary_unit via secondsToDuration() (z. B. 08:00 Std.).

8.2 Generierungs-Dialog (SalaryElementGenerationDialogComponent)​

Verzeichnis: salary-managment/salary-elements/salary-element-generation-dialog/ (neu)

  • Ausgelöst über einen „Lohnelemente generieren"-Button (autorenew-Icon) in salary-elements.component.html, gated durch userIsAllowed().
  • Monatsauswahl auf das aktuelle Jahr beschränkt (startView="year"); Mehrfach-Mitarbeiterauswahl (Suche via searchWorker).
  • generate() bestätigt über DeleteConfirmDialogV2Service (i18n SalaryElements.generateConfirm/generateConfirmSub), dann runGeneration(): month = getMonth()+1 und ein Request pro Mitarbeiter in Promise.all.
  • Service SalaryElementsSerivice.generateSalaryElements(workerId, month) → POST api/salaryElements/generate/worker/{workerId}?month={month} (leerer Body).

Warnhinweis-Semantik: Der Bestätigungstext weist darauf hin, dass bestehende Lohnelemente im gewählten Zeitraum ersetzt werden – exakt das Verhalten des Processors (Delete-vor-Neuanlage, Kap. 4.2).

8.3 Salary-Element-Listen​

  • Admin-Liste (salary-elements.component): Spalten ['worker','salaryType','profileDate','salaryUnit','transferToPayroll','edit']; Monats-Datumsfilter (SalaryElements.profileDate); CRUD-Dialog + Generierungs-Button. salaryUnit als Dauer gerendert.
  • Mitarbeiter-Liste (worker-salary-elements.component, eingebettet in Worker-Details): wie oben ohne worker-Spalte; GET salaryElements/worker/{id}.
  • Zulagen-Report (hrm/worker-allowances): Monats-Picker + Typfilter (find/salaryTypes?type=allowance), Inline-Edit von Betrag/Freigabe. Kein Generierungs-Button (reiner Altpfad-Report).

8.4 Lohnart-Dialog (SalaryTypeDialogComponent)​

  • Felder: description (Pflicht), amount (Pflicht, Label „Zuschlag in %"), minimal_time (Label „Minimum", type="time", Sekunden-Konvertierung), shouldIncludeInExport, externalSalaryType, is_standard_for_holiday (Label „Standard für Feiertage").
  • Cross-Field-Regel: Wird is_standard_for_holiday aktiviert, wird shouldIncludeInExport auf true erzwungen und deaktiviert; Deaktivieren gibt es wieder frei.
  • types = ['hours','salary','allowance'] (einzige Quelle der Typwerte).

8.5 Lohnverträge (salary-agreements/, salary-agreement-day/)​

  • Liste salary-agreements.component: Spalten ['name','description','edit']; Zeilen → Detailroute; neuer „Mitglieder"-Button (openWorkerDialog).
  • Vertrags-Dialog salary-agreements/dialogs/salary-agreement-dialog/: Felder name, description, over_time_salary_type (vorbefüllt). Hinweis: getTransferObject sendet nur name/description – die Salary-Type-Referenzen werden im Dialog gelesen, aber nicht mitgespeichert.
  • Mitglieder-Dialog salary-agreements/dialogs/salary-agreement-worker-dialog/: ordnet Mitarbeiter dem Vertrag zu via PUT api/v2/workers/{workerid} ({ salary_agreement: {id}|null }); GET api/v2/workers/bySalaryAgreement/{id}.
  • Tages-Lines salary-agreement-day.component: Spalten ['description','salaryType','startTime','endTime','edit']; Line-Editor mit salary_type (Pflicht), start_time/end_time (type=time, Pflicht), description.
  • Service salary-agreements.service.ts: salaryAgreements (CRUD), find/salaryAgreements, v2/workers (Zuordnung).

8.6 Abwesenheitscode ↔ Lohnart​

  • hrm/absenceCode/absenceCode.model.ts: neues Feld salary_type.
  • absenceCodeDialog: neuer suchbarer Lohnart-Select (api/find/salaryTypes?query=); Payload enthält salary_type: { id }. Dieser Link speist den Abwesenheitspfad des Generators (Kap. 4.3).

8.7 i18n​

Nur drei neue Keys unter SalaryElements (salary-managment/i18n/de.ts, analog en/pl):

generate           : 'Lohnelemente generieren'
generateConfirm : 'Bestehende Lohnelemente im gewählten Zeitraum werden … neu generiert und ersetzt. Fortfahren?'
generateConfirmSub : 'Bereits generierte Lohnelemente in diesem Zeitraum gehen verloren.'

Die Labels der neuen SalaryType-Felder (Zuschlag in %, Minimum, Standard für Feiertage) und der Abwesenheitscode-Select (Lohnart) sind hartkodiertes Deutsch in den Templates – Lokalisierungslücke.


9. Berechtigungssteuerung​

Datei: salary-managment/salary-elements/salary-element-creation-access.service.ts

  • getAuthorization() → POST auth/me; setzt hasAccess = true, sobald ein Permission-Name auf /create_salary_elements/gi matcht.
  • Gated (nur UI-Sichtbarkeit): Create-FAB, Edit/Delete und der neue Generierungs-Button. Erforderliche Permission: create_salary_elements. Serverseitige Durchsetzung erfolgt unabhängig davon.

10. Durchgerechnetes Beispiel (journalbasiert)​

Setup: Mitarbeiter mit SalaryAgreement. Vertragstag „Friday" hat zwei Lines: 06:00–22:00 → Normalzeit, 22:00–06:00 → Nachtzuschlag. Lohnart „Feiertag" hat isStandardForHoliday = true. Tag T = 2026-07-10 (Freitag), Standardprofil (0_24).

Aufruf: POST api/salaryElements/generate/worker/{id}?month=7 → iteriert alle Julitage; für den 10.07.:

  1. Journal 10.07. gefunden mit isCalculated = true, isApproved = true → weiter (sonst Skip).
  2. Vorhandene Elemente mit journalDate = 10.07. gelöscht.
  3. Keine Abwesenheit am Tag → kein Abwesenheitspfad.
  4. Buchungen im Fenster 10.07. 00:00…23:59: Check-In 07:58, Pausen-Buchungen 12:00/12:30, Check-Out 20:03.
  5. Anwesenheits-Intervalle gebildet, Pause 12:00–12:30 als isBreakTime.
  6. Kein Feiertag am 10.07. → keine virtuelle Line.
  7. Überlappung mit Line 06:00–22:00 (Normalzeit): ~12 h; mit 22:00–06:00 (Nachtzuschlag): 0.
  8. sumOverlapTimes: Normalzeit-Total ≥ minimalTime → bleibt; Pause 30 min wird in calculateBreakTime ermittelt.
  9. createSalaryElements: ein SalaryElement (Normalzeit), profileDate = 10.07., journalDate = 10.07., salaryUnit ≈ 12 h − 0,5 h = 11,5 h in Sekunden.

Feiertagsfall: Wäre der 10.07. ein Feiertag, käme zusätzlich eine virtuelle Ganztags-Line mit der isStandardForHoliday-Lohnart hinzu → entsprechender Feiertagsposten.

Nachtschicht-Fall (12_12): Fenster 09.07. 12:00 … 10.07. 12:00; über Mitternacht laufende Intervalle würden an 00:00 gesplittet und ggf. auf profileDate 09.07. und 10.07. verteilt – beide mit journalDate = 10.07..


11. Fallstricke, Grenzen & offene Punkte​

ThemaBeschreibungOrt
Zwei Subsysteme, eine EntityJournalpfad (Sekunden, journalDate gesetzt) und Zulagenpfad (Pauschale, journalDate = null) schreiben beide in SalaryElement. salaryUnit ist kontextabhängig zu interpretieren.Processor vs. Command
Voraussetzung isCalculated + isApprovedJournalpfad generiert nur für Tage mit berechnetem und freigegebenem Journal; sonst stiller Skip ohne Rückmeldung.generate() Z. 144–150
TestCommand defektRuft nicht existente Methode generateSalaryElementsForWorkerForDay (richtiger Name: generateSalaryElementsForDayForWorker, andere Arg-Reihenfolge). Nicht lauffähig.TestCommand.php
Kein SchedulerWeder Journal- noch Zulagenpfad sind zeitgesteuert; Journalpfad ist rein UI-getriggert (pro Mitarbeiter, pro Monat). Eine „alle Mitarbeiter"-Massengenerierung ist im Backend vorhanden (generateSalaryElementsForMonth), aber nicht per Route exponiert.Controller/Processor
Line-Felder ungenutztSalaryAgreementLine.minimalTime/maximalTime/secondarySalaryType/secondaryFactor werden vom Processor nicht ausgewertet (nur SalaryType.minimalTime greift).Processor
Agreement-SalaryTypes ungenutzt/ungespeichertwageSalary/overTimeSalary/defaultSalary am Vertrag werden vom Processor nicht konsumiert; der FE-Vertragsdialog speichert sie zudem nicht (nur name/description).Entity + FE-Dialog
Feiertag global (nicht mandanten-/regionsspezifisch)publicHolidayRepo->findOneBy(['date' => $calendarDay]) prüft nur das Datum – keine Region/Mandant.generate()
getMonthRange Jahres-DefaultOhne year wird das aktuelle Jahr genommen; Generierung für vergangene Jahre nur über die interne API mit year-Argument.getMonthRange
Keine type-ValidierungSalaryType.type bleibt ein freier String ohne Enum-Constraint; Gültigkeit nur über FE-Dropdown.Entity
Hartkodierte FE-Labels„Zuschlag in %", „Minimum", „Standard für Feiertage", „Lohnart" nicht übersetzt.FE-Templates
journalDate nur BackendDas Frontend kennt kein journal_date; Debugging von Nachtschicht-Verteilungen nur über die DB möglich.FE-Modelle
Namens-TippfehlerCommand …saleryelements…, Klasse GenerateMounthlySalaryElementsCommand, FE-Service SalaryElementsSerivice.diverse
Keine TestsFür die Generierungslogik existieren keine automatisierten Tests.–

12. Referenz: alle Fundstellen​

Backend​

DateiRolle
Processor/Salary/SalaryElementGenerationProcessor.phpJournalbasierte Generierung (Kern)
ViewModel/PreCalculatedBooking.phpRechenobjekt Anwesenheits-Intervall
Controller/SalaryElementController.phpCRUD + generate/worker/{id}
Command/GenerateMounthlySalaryElementsCommand.phpZulagen-Monatsgenerierung (Altpfad)
Command/TestCommand.phpTest-Aufruf (defekt)
Entity/SalaryElement.phpprofileDate, journalDate, salaryUnit
Entity/SalaryType.phptype, amount, minimalTime, isStandardForHoliday, absenceCodes
Entity/SalaryAgreement.phpLohnvertrag
Entity/SalaryAgreementDay.phpWochentagsregeln
Entity/SalaryAgreementLine.phpZeitfenster → Lohnart
Entity/AbsenceCode.phpsalaryType-Verknüpfung
Entity/Worker.phpsalaryAgreement, salaryElements
Entity/WorkerSalaryType.phpZulagen-Zuordnung (Altpfad)
Repository/SalaryElementRepository.phpgetByWorkerAndRange, getByWorkerAndJournalDate
Repository/AbsenceRepository.phpgetAbsencesForWorkerInRangeByType (type optional)
Repository/WorkerRepository.phpgetWorkersByType, getActiveWorkers
migrations Version20260302140928 / …319151007 / …323104345 / …630141454Schema

Frontend (relativ zu biz.Suite_Frontend/src/app/main/apps/)​

Datei/VerzeichnisRolle
organization-management/salary-managment/salary-elements/salary-element-generation-dialog/Generierungs-Dialog (Trigger)
organization-management/salary-managment/salary-elements/salary-elements.service.tsgenerateSalaryElements → POST generate
organization-management/salary-managment/salary-elements/Admin-Liste + Dialog + Access-Service
organization-management/salary-managment/salary-types/Lohnart-Modell + Dialog (amount/minimal_time/is_standard_for_holiday)
organization-management/salary-managment/salary-agreements/ + dialogs/Verträge, Mitglieder-Zuordnung
organization-management/salary-managment/salary-agreement-day/Vertrags-Lines
hrm/absenceCode/Abwesenheitscode ↔ Lohnart
hrm/worker-allowances/Zulagen-Report (Altpfad)
hrm/workers/ (worker.model.ts, details/worker-referenced-salary-types/)salary_agreement, Zulagen-Zuordnung

Ende der Dokumentation.